home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / lib / python2.6 / dist-packages / launchpadbugs / html_bug.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2009-04-20  |  63.8 KB  |  1,612 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.6)
  3.  
  4. """
  5. TODO:
  6.     * TODOs are specified in the classes
  7.     * being more verbose in 'container'-object's __repr__: add bugnumber to each object
  8.     * standardize *.changed (output: frozenset/list, property)
  9.     * how to handle global attachment-related variables?
  10.     * add revert()-function(s)
  11. THIS IS STILL WORK IN PROGRESS!!
  12. """
  13. import re
  14. import os
  15. import libxml2
  16. import exceptions
  17. from exceptions import parse_error
  18. from bugbase import Bug as BugBase
  19. from lptime import LPTime
  20. from tasksbase import LPTasks, LPTask
  21. from attachmentsbase import LPAttachments, LPAttachment
  22. from commentsbase import LPComments, LPComment
  23. from lphelper import user, product, change_obj, unicode_for_libxml2
  24. from lpconstants import BUG, BASEURL
  25. from subscribersbase import LPSubscribers
  26. from utils import valid_lp_url
  27.  
  28. def noerr(ctx, str):
  29.     pass
  30.  
  31. libxml2.registerErrorHandler(noerr, None)
  32.  
  33. def _small_xpath(xml, expr):
  34.     ''' Returns the content of the first result of a xpath expression  '''
  35.     result = xml.xpathEval(expr)
  36.     if not result:
  37.         return False
  38.     return result[0].content
  39.  
  40.  
  41. def get_bug(id):
  42.     return Bug._container_refs[id]
  43.  
  44.  
  45. def _blocked(func, error = None):
  46.     
  47.     def f(a, *args, **kwargs):
  48.         
  49.         try:
  50.             x = a.infotable.current
  51.         except AttributeError:
  52.             e = None
  53.             error = '%s %s' % (f.error, e)
  54.             raise AttributeError, error
  55.  
  56.         return func(a, *args, **kwargs)
  57.  
  58.     if not error:
  59.         pass
  60.     f.error = 'Unable to get current InfoTable row.'
  61.     return f
  62.  
  63.  
  64. def _attr_ext(i, s):
  65.     if i.startswith('__'):
  66.         return '_%s%s' % (s.__class__.__name__, i)
  67.     return i
  68.  
  69.  
  70. def _gen_getter(attr):
  71.     ''' Returns a function to return the value of an attribute
  72.     
  73.     Example:
  74.         get_example = _gen_getter("x.y")
  75.     is like:
  76.         def get_example(self):
  77.             if not x.parsed:
  78.                 x.parse()
  79.             return x.y
  80.     
  81.     '''
  82.     
  83.     def func(s):
  84.         attributes = attr.split('.')
  85.         attributes = (map,)((lambda a: _attr_ext(a, s)), attributes)
  86.         x = getattr(s, attributes[0])
  87.         attributes.insert(0, s)
  88.         if not x.parsed:
  89.             x.parse()
  90.         
  91.         return reduce(getattr, attributes)
  92.  
  93.     return func
  94.  
  95.  
  96. def _gen_setter(attr):
  97.     ''' Returns a function to set the value of an attribute
  98.     
  99.     Example:
  100.         set_example = _gen_setter("x.y")
  101.     is like:
  102.         def set_example(self, value):
  103.             if not x.parsed:
  104.                 x.parse()
  105.             x.y = value
  106.     
  107.     '''
  108.     
  109.     def func(s, value):
  110.         attributes = attr.split('.')
  111.         attributes = (map,)((lambda a: _attr_ext(a, s)), attributes)
  112.         x = getattr(s, attributes[0])
  113.         attributes.insert(0, s)
  114.         if not x.parsed:
  115.             x.parse()
  116.         
  117.         setattr(reduce(getattr, attributes[:-1]), attributes[-1], value)
  118.  
  119.     return func
  120.  
  121.  
  122. def create_project_task(project):
  123.     x = [
  124.         None] * 14
  125.     task = Info(product(project), *x)
  126.     task._type = '%s.product' % project
  127.     return task
  128.  
  129.  
  130. def create_distro_task(distro = None, sourcepackage = None, url = None):
  131.     x = [
  132.         None] * 14
  133.     if distro is None:
  134.         distro = 'Ubuntu'
  135.     
  136.     if sourcepackage is not None:
  137.         affects = product('%s (%s)' % (sourcepackage, distro.title()))
  138.     else:
  139.         affects = product(distro.title())
  140.     task = Info(affects, *x)
  141.     task._type = '%s.sourcepackagename' % affects
  142.     task._remote = url
  143.     return task
  144.  
  145.  
  146. class Info(LPTask):
  147.     """ The 'Info'-object represents one row of the 'InfoTable' of a bugreport
  148.     
  149.     * editable attributes:
  150.         .sourcepackage: lp-name of a package/project
  151.         .status: valid lp-status
  152.         .importance: valid lp-importance (if the user is not permitted to
  153.             change 'importance' an 'IOError' will be raised
  154.         .assignee: lp-login of an user/group
  155.         .milestone: value must be in '.valid_milestones'
  156.         
  157.     * read-only attributes:
  158.         .affects, .target, .valid_milestones
  159.         
  160.     TODO: * rename 'Info' into 'Task'
  161.     """
  162.     
  163.     def __init__(self, affects, status, importance, assignee, current, editurl, type, milestone, available_milestone, lock_importance, targeted_to, remote, editlock, edit_fields, connection):
  164.         data_dict = {
  165.             'affects': affects,
  166.             'status': status,
  167.             'importance': importance,
  168.             'assignee': assignee,
  169.             'current': current,
  170.             'editurl': editurl,
  171.             'type': type,
  172.             'milestone': milestone,
  173.             'available_milestone': available_milestone,
  174.             'lock_importance': lock_importance,
  175.             'targeted_to': targeted_to,
  176.             'remote': remote,
  177.             'editlock': editlock,
  178.             'edit_fields': edit_fields,
  179.             'connection': connection }
  180.         LPTask.__init__(self, data_dict)
  181.         self._cache = {
  182.             'sourcepackage': self._sourcepackage,
  183.             'status': self._status,
  184.             'importance': self._importance,
  185.             'assignee': self._assignee,
  186.             'milestone': self._milestone }
  187.  
  188.     
  189.     def set_sourcepackage(self, package):
  190.         if self._editlock:
  191.             raise IOError, "The sourcepackage of this bug can't be edited, maybe because this bug is a duplicate of an other one"
  192.         self._editlock
  193.         self._sourcepackage = package
  194.  
  195.     
  196.     def set_assignee(self, lplogin):
  197.         if self._editlock:
  198.             raise IOError, "The assignee of this bug can't be edited, maybe because this bug is a duplicate of an other one"
  199.         self._editlock
  200.         if self._remote:
  201.             raise IOError, 'This task is linked to a remote-bug system, please change the assignee there'
  202.         self._remote
  203.         self._assignee = lplogin
  204.  
  205.     
  206.     def set_status(self, status):
  207.         if self._editlock:
  208.             raise IOError, "The status of this bug can't be edited, maybe because this bug is a duplicate of an other one"
  209.         self._editlock
  210.         if self._remote:
  211.             raise IOError, 'This task is linked to a remote-bug system, please change the status there'
  212.         self._remote
  213.         if status not in BUG.STATUS.values():
  214.             raise ValueError, "Unknown status '%s', status must be one of these: %s" % (status, BUG.STATUS.values())
  215.         status not in BUG.STATUS.values()
  216.         self._status = status
  217.  
  218.     
  219.     def set_importance(self, importance):
  220.         if self._editlock:
  221.             raise IOError, "The importance of this bug can't be edited, maybe because this bug is a duplicate of an other one"
  222.         self._editlock
  223.         if self._remote:
  224.             raise IOError, 'This task is linked to a remote-bug system, please change the importance there'
  225.         self._remote
  226.         if self._lock_importance:
  227.             raise IOError, "'Importance' changeable only by a project maintainer or bug contact"
  228.         self._lock_importance
  229.         if importance not in BUG.IMPORTANCE.values():
  230.             raise ValueError, "Unknown importance '%s', importance must be one of these: %s" % (importance, BUG.IMPORTANCE.values())
  231.         importance not in BUG.IMPORTANCE.values()
  232.         self._importance = importance
  233.  
  234.     
  235.     def set_milestone(self, milestone):
  236.         if self._editlock:
  237.             raise IOError, "The milestone of this bug can't be edited, maybe because this bug is a duplicate of an other one"
  238.         self._editlock
  239.         if not self._Info__available_milestone:
  240.             raise ValeError, 'No milestones defined for this product'
  241.         self._Info__available_milestone
  242.         if milestone not in self._available_milestone:
  243.             raise ValueError, 'Unknown milestone, milestone must be one of these: %s' % self._available_milestone
  244.         milestone not in self._available_milestone
  245.         self._milestone = milestone
  246.  
  247.     
  248.     def commit(self, force_changes = False, ignore_lp_errors = True):
  249.         """ Commits the local changes to launchpad.net
  250.         
  251.         * force_changes: general argument, has not effect in this case
  252.         * ignore_lp_errors: if the user tries to commit invalid data to launchpad,
  253.             launchpad returns an error-page. If 'ignore_lp_errors=False' Info.commit()
  254.             will raise an 'ValueError' in this case, otherwise ignore this
  255.             and leave the bugreport in launchpad unchanged (default=True)
  256.         """
  257.         changed = self.changed
  258.         if changed:
  259.             if self._type:
  260.                 full_sourcepackage = self._type[:self._type.rfind('.')]
  261.             else:
  262.                 full_sourcepackage = '%s_%s' % (self.targeted_to, str(self._affects))
  263.             s = self.sourcepackage
  264.             if s == 'ubuntu':
  265.                 s = ''
  266.             
  267.             args = {
  268.                 '%s.actions.save' % full_sourcepackage: '1',
  269.                 '%s.comment_on_change' % full_sourcepackage: '' }
  270.             if self._type:
  271.                 args[self._type] = s
  272.             
  273.             if '.status' in self._edit_fields:
  274.                 args['%s.status-empty-marker' % full_sourcepackage] = '1'
  275.                 args['%s.status' % full_sourcepackage] = self.status
  276.             
  277.             if '.importance' in self._edit_fields:
  278.                 args['%s.importance' % full_sourcepackage] = self.importance
  279.                 args['%s.importance-empty-marker' % full_sourcepackage] = '1'
  280.             
  281.             if '.milestone' in self._edit_fields:
  282.                 args['%s.mlestone' % full_sourcepackage] = ''
  283.                 args['%s.milestone-empty-marker' % full_sourcepackage] = '1'
  284.             
  285.             args['%s.assignee.option' % full_sourcepackage] = '%s.assignee.assign_to' % full_sourcepackage
  286.             if not self.assignee:
  287.                 pass
  288.             args['%s.assignee' % full_sourcepackage] = ''
  289.             result = self._connection.post(self._editurl, args)
  290.             if result.url.endswith('+editstatus') and not ignore_lp_errors:
  291.                 raise exceptions.PythonLaunchpadBugsValueError({
  292.                     'arguments': args }, self._editurl, 'one or more arguments might be wrong')
  293.             not ignore_lp_errors
  294.         
  295.  
  296.  
  297.  
  298. class InfoTable(LPTasks):
  299.     """ The 'InfoTable'-object represents the tasks at the top of a bugreport
  300.         
  301.     * read-only attributes:
  302.         .current: returns the highlighted Info-object of the bugreport
  303.     
  304.     TODO:  * rename 'InfoTable' into 'TaskTable'
  305.            * allow adding of tasks (Also affects upstream/Also affects distribution)
  306.            * does current/tracked work as expected?
  307.            * remote: parse editable values
  308.     """
  309.     
  310.     def __init__(self, connection, xml, url):
  311.         LPTasks.__init__(self, {
  312.             'connection': connection,
  313.             'url': url,
  314.             'xml': xml })
  315.         self._InfoTable__url = url
  316.         self._InfoTable__xml = xml
  317.  
  318.     
  319.     def parse(self):
  320.         """ Parsing the info-table
  321.         
  322.         * format:  'Affects'|'Status'|'Importance'|'Assigned To'
  323.         
  324.         TODO: * working on 'tracked in...' - currently there is only one 'tracked in'
  325.                 entry per bugreport supported
  326.               * REMOTE BUG!!!
  327.         """
  328.         if self.parsed:
  329.             return True
  330.         rows = self._InfoTable__xml[0].xpathEval('tbody/tr[not(@style="display: none") and not(@class="secondary")]')
  331.         parse_error(self._InfoTable__xml[0], 'InfoTable.rows', xml = self._InfoTable__xml, url = self._InfoTable__url)
  332.         highl_target = None
  333.         temp_status = BUG.STATUS.copy()
  334.         temp_status['statusUNKNOWN'] = 'Unknown'
  335.         temp_importance = BUG.IMPORTANCE.copy()
  336.         temp_importance['importanceUNKNOWN'] = 'Unknown'
  337.         tracked = False
  338.         affects = product(None)
  339.         for row in rows:
  340.             edit_fields = set()
  341.             tmp_affects = affects
  342.             current = False
  343.             remote = None
  344.             if row.prop('class') == 'highlight':
  345.                 current = True
  346.             
  347.             row_cells = row.xpathEval('td')
  348.             row_cells = _[1]
  349.             affects = product(affects_lpname, affects_longname, affects_type)
  350.             tracked = False
  351.             targeted_to = None
  352.             if row_cells[0].xpathEval('img[@alt="Targeted to"]'):
  353.                 targeted_to = row_cells[0].xpathEval('a')[0].content
  354.                 affects = tmp_affects
  355.                 if highl_target:
  356.                     if targeted_to.lower() == highl_target.lower():
  357.                         current = True
  358.                     
  359.                 
  360.             
  361.             xmledit = row.xpathEval('following-sibling::tr[@style="display: none"][1]')
  362.             type = None
  363.             milestone = None
  364.             available_milestone = { }
  365.             editurl = None
  366.             editlock = False
  367.             lock_importance = False
  368.             if xmledit:
  369.                 xmledit = xmledit[0]
  370.                 editurl = xmledit.xpathEval('td/form')
  371.                 parse_error(editurl, 'InfoTable.editurl', xml = xmledit, url = self._InfoTable__url)
  372.                 editurl = valid_lp_url(editurl[0].prop('action'), BASEURL.BUG)
  373.                 if xmledit.xpathEval('descendant::label[contains(@for,"bugwatch")]'):
  374.                     x = xmledit.xpathEval('descendant::label[contains(@for,"bugwatch")]/a')
  375.                     if x:
  376.                         remote = x[0].prop('href')
  377.                     else:
  378.                         remote = True
  379.                 
  380.                 if not remote:
  381.                     for i in [
  382.                         'product',
  383.                         'sourcepackagename']:
  384.                         x = xmledit.xpathEval('td/form/div//table//input[contains(@id,".%s")]' % i)
  385.                         if x:
  386.                             type = x[0].prop('id')
  387.                             continue
  388.                     
  389.                     if not type:
  390.                         if not row.xpathEval('td[2]//img[contains(@src,"milestone")]'):
  391.                             parse_error(False, 'InfoTable.type.milestone', xml = xmledit, url = self._InfoTable__url)
  392.                         
  393.                     
  394.                     m = xmledit.xpathEval('descendant::select[contains(@id,".milestone")]//option')
  395.                     if m:
  396.                         for i in m:
  397.                             available_milestone[i.prop('value')] = i.content
  398.                             if i.prop('selected'):
  399.                                 milestone = i.content
  400.                                 continue
  401.                         
  402.                     
  403.                     m = xmledit.xpathEval('descendant::td[contains(@title, "Changeable only by a project maintainer") and count(span)=0]')
  404.                     if len(m) == 1 and not milestone:
  405.                         milestone = m[0].content.strip('\n ')
  406.                     
  407.                 
  408.                 if not xmledit.xpathEval('descendant::select[contains(@id,".importance")]'):
  409.                     lock_importance = True
  410.                 
  411.                 m = set([
  412.                     '.sourcepackagename',
  413.                     '.product',
  414.                     '.status',
  415.                     '.status-empty-marker',
  416.                     '.importance',
  417.                     '.importance-empty-marker',
  418.                     '.milestone',
  419.                     '.milestone-empty-marker'])
  420.                 for i in m:
  421.                     x = xmledit.xpathEval('td/form//input[contains(@name,"%s")]' % i)
  422.                     y = xmledit.xpathEval('td/form//select[contains(@name,"%s")]' % i)
  423.                     if x or y:
  424.                         edit_fields.add(i)
  425.                         continue
  426.                 
  427.             else:
  428.                 editlock = True
  429.             parse_error(row_cells[1], 'InfoTable.status.1', xml = row_cells, url = self._InfoTable__url)
  430.             parse_error(row_cells[1].prop('class') in temp_status, 'InfoTable.status.2', msg = "unknown bugstatus '%s' in InfoTable.parse()" % row_cells[1].prop('class'), error_type = exceptions.VALUEERROR, url = self._InfoTable__url)
  431.             status = temp_status[row_cells[1].prop('class')]
  432.             parse_error(row_cells[2], 'InfoTable.importance.1', xml = row_cells, url = self._InfoTable__url)
  433.             parse_error(row_cells[2].prop('class') in temp_importance, 'InfoTable.importance.2', msg = "unknown bugimportance '%s' in InfoTable.parse()" % row_cells[2].prop('class'), error_type = exceptions.VALUEERROR, url = self._InfoTable__url)
  434.             importance = temp_importance[row_cells[2].prop('class')]
  435.             assignee = row_cells[3].xpathEval('a')
  436.             if assignee:
  437.                 assignee = assignee[0]
  438.                 if remote:
  439.                     assignee = [](_[2])
  440.                 else:
  441.                     assignee = user.parse_html_user(assignee)
  442.             else:
  443.                 assignee = user(None)
  444.             if current:
  445.                 self._current = len(self)
  446.             
  447.             if not editurl:
  448.                 pass
  449.             self.append(Info(affects, status, importance, assignee, current, self._url, type, milestone, available_milestone, lock_importance, targeted_to, remote, editlock, edit_fields, connection = self._connection))
  450.         
  451.         self.parsed = True
  452.         return True
  453.  
  454.     
  455.     def _LP_create_task(self, task, force_changes, ignore_lp_errors):
  456.         if not task.component._type:
  457.             pass
  458.         tsk = ''
  459.         if tsk.endswith('.product'):
  460.             url = '%s/+choose-affected-product' % self._url
  461.             args = {
  462.                 'field.visited_steps': 'choose_product, specify_remote_bug_url',
  463.                 'field.product': str(task.component.affects),
  464.                 'field.actions.continue': 'Add to Bug Report' }
  465.             result = self._connection.post(url, args)
  466.             if result.url == url:
  467.                 raise exceptions.choose_pylpbugsError(error_type = exceptions.VALUEERROR, text = result.text, url = url)
  468.             result.url == url
  469.         elif tsk.endswith('.sourcepackagename'):
  470.             url = '%s/+distrotask' % self._url
  471.             if not task.component.target:
  472.                 pass
  473.             if not task.component.sourcepackage:
  474.                 pass
  475.             if not task.component.remote:
  476.                 pass
  477.             args = {
  478.                 'field.distribution': 'ubuntu',
  479.                 'field.distribution-empty-marker': 1,
  480.                 'field.sourcepackagename': '',
  481.                 'field.visited_steps': 'specify_remote_bug_url',
  482.                 'field.bug_url': '',
  483.                 'field.actions.continue': 'Continue' }
  484.             result = self._connection.post(url, args)
  485.             if result.url == url:
  486.                 raise exceptions.choose_pylpbugsError(error_type = exceptions.VALUEERROR, text = result.text, url = url)
  487.             result.url == url
  488.         else:
  489.             raise NotImplementedError
  490.         return tsk.endswith('.product')
  491.  
  492.  
  493.  
  494. class BugReport(object):
  495.     """ The 'BugReport'-object is the report itself
  496.     
  497.     * editable attributes:
  498.         .description: any text
  499.         .title/.summary: any text
  500.         .tags: list, use list operations to add/remove tags
  501.         .nickname
  502.         
  503.     * read-only attributes:
  504.         .target: e.g. 'upstream'
  505.         .sourcepackage: 'None' if not package specified
  506.         .reporter, .date
  507.     """
  508.     
  509.     def __init__(self, connection, xml, url):
  510.         (self._BugReport__title, self._BugReport__description, self._BugReport__tags, self._BugReport__nickname, self.target, self.sourcepackage, self.reporter, self.date) = [
  511.             None] * 8
  512.         self._BugReport__cache = { }
  513.         self.parsed = False
  514.         self._BugReport__connection = connection
  515.         self._BugReport__xml = xml
  516.         self._BugReport__url = url
  517.         self._BugReport__description_raw = None
  518.  
  519.     
  520.     def __repr__(self):
  521.         return '<BugReport>'
  522.  
  523.     
  524.     def parse(self):
  525.         if self.parsed:
  526.             return True
  527.         description = self._BugReport__xml.xpathEval('//body//div[@class="report"]/div[@id="bug-description"]')
  528.         parse_error(description, 'BugReport.description', xml = self._BugReport__xml, url = self._BugReport__url)
  529.         p = description[0].xpathEval('p')
  530.         description = ''
  531.         for i in p[:-1]:
  532.             description = ''.join([
  533.                 description,
  534.                 i.content,
  535.                 '\n\n'])
  536.         
  537.         description = ''.join([
  538.             description,
  539.             p[-1:].pop().content])
  540.         self._BugReport__description = description
  541.         title = self._BugReport__xml.xpathEval('//title')
  542.         parse_error(title, 'BugReport.title', self._BugReport__xml, self._BugReport__url)
  543.         titleFilter = 'Bug #[0-9]* in ([^:]*?): (.*)'
  544.         title = re.findall(titleFilter, title[0].content)
  545.         parse_error(title, 'BugReport.__title', url = self._BugReport__url)
  546.         self._BugReport__title = title[0][1].rstrip('\xe2\x80\x9d').lstrip('\xe2\x80\x9c')
  547.         target = title[0][0].split(' ')
  548.         if len(target) == 2:
  549.             self.target = target[1].lstrip('(').rstrip(')')
  550.         
  551.         self.sourcepackage = target[0]
  552.         if self.sourcepackage == 'Ubuntu':
  553.             self.sourcepackage = None
  554.         
  555.         tags = self._BugReport__xml.xpathEval('//body//div[@class="report"]//div[@id="bug-tags"]//a')
  556.         self._BugReport__tags = [ i.content for i in tags ]
  557.         m = self._BugReport__xml.xpathEval('//span[@class="object identifier"]')
  558.         if not m:
  559.             pass
  560.         m = self._BugReport__xml.xpathEval('//div[@class="object identifier"]')
  561.         parse_error(m, 'BugReport.__nickname', xml = self._BugReport__xml, url = self._BugReport__url)
  562.         r = re.search('\\(([^\\)]+)\\)', m[0].content)
  563.         if not r or r.group(1):
  564.             pass
  565.         self._BugReport__nickname = None
  566.         d = self._BugReport__xml.xpathEval('//span[@class="object timestamp"]/span')
  567.         if not d:
  568.             pass
  569.         d = self._BugReport__xml.xpathEval('//p[@class="object timestamp"]/span')
  570.         parse_error(d, 'BugReport.date', xml = m[0], url = self._BugReport__url)
  571.         self.date = LPTime(d[0].prop('title'))
  572.         d = self._BugReport__xml.xpathEval('//span[@class="object timestamp"]/a')
  573.         if not d:
  574.             pass
  575.         d = self._BugReport__xml.xpathEval('//p[@class="object timestamp"]/a')
  576.         parse_error(d, 'BugReport.reporter', xml = m[0], url = self._BugReport__url)
  577.         self.reporter = user.parse_html_user(d[0])
  578.         self._BugReport__cache = {
  579.             'title': self._BugReport__title,
  580.             'description': self._BugReport__description,
  581.             'tags': self._BugReport__tags[:],
  582.             'nickname': self._BugReport__nickname }
  583.         self.parsed = True
  584.         return True
  585.  
  586.     
  587.     def get_title(self):
  588.         return self._BugReport__title
  589.  
  590.     
  591.     def set_title(self, title):
  592.         self._BugReport__title = title
  593.  
  594.     title = property(get_title, set_title, doc = 'title of a bugreport')
  595.     
  596.     def get_description(self):
  597.         return self._BugReport__description
  598.  
  599.     
  600.     def set_description(self, description):
  601.         self._BugReport__description = description
  602.  
  603.     description = property(get_description, set_description, doc = 'description of a bugreport')
  604.     
  605.     def tags(self):
  606.         return self._BugReport__tags
  607.  
  608.     tags = property(tags)
  609.     
  610.     def get_nickname(self):
  611.         return self._BugReport__nickname
  612.  
  613.     
  614.     def set_nickname(self, nickname):
  615.         self._BugReport__nickname = nickname
  616.  
  617.     nickname = property(get_nickname, set_nickname, doc = 'nickname of a bugreport')
  618.     
  619.     def changed(self):
  620.         changed = set()
  621.         for k in self._BugReport__cache:
  622.             if self._BugReport__cache[k] != getattr(self, k):
  623.                 changed.add(change_obj(k))
  624.                 continue
  625.         
  626.         return frozenset(changed)
  627.  
  628.     changed = property(changed)
  629.     
  630.     def description_raw(self):
  631.         if not self._BugReport__description_raw:
  632.             url = '%s/+edit' % self._BugReport__url
  633.             result = self._BugReport__connection.get(url)
  634.             xmldoc = libxml2.htmlParseDoc(unicode_for_libxml2(result.text), 'UTF-8')
  635.             x = xmldoc.xpathEval('//textarea[@name="field.description"]')
  636.             parse_error(x, 'BugReport.description_raw', xml = xmldoc, url = url)
  637.             self._BugReport__description_raw = x[0].content
  638.         
  639.         return self._BugReport__description_raw
  640.  
  641.     description_raw = property(description_raw)
  642.     
  643.     def commit(self, force_changes = False, ignore_lp_errors = True):
  644.         """ Commits the local changes to launchpad.net
  645.         
  646.         * force_changes: if a user adds a tag which has not been used before
  647.             and force_changes is True then commit() tries to create a new
  648.             tag for this package; if this fails or force_changes=False
  649.             commit will raise a 'ValueError'
  650.         * ignore_lp_errors: if the user tries to commit invalid data to launchpad,
  651.             launchpad returns an error-page. If 'ignore_lp_errors=False' Info.commit()
  652.             will raise an 'ValueError' in this case, otherwise ignore this
  653.             and leave the bugreport in launchpad unchanged (default=True)
  654.         """
  655.         if self.changed:
  656.             if not self.title and description:
  657.                 raise exceptions.PythonLaunchpadBugsValueError(msg = "To change a bugreport 'description' and 'title' don't have to be empty", url = self._BugReport__url)
  658.             description
  659.             args = {
  660.                 'field.actions.change': '1',
  661.                 'field.title': self.title,
  662.                 'field.description': description,
  663.                 'field.tags': ' '.join(self.tags),
  664.                 'field.name': nickname }
  665.             url = '%s/+edit' % self._BugReport__url
  666.             result = self._BugReport__connection.post(url, args)
  667.             if result.url == url:
  668.                 pass
  669.             None if [] not in [ i.component for i in self.changed ] else 'description' if [] not in [ i.component for i in self.changed ] else 'nickname' if not ignore_lp_errors or force_changes else force_changes
  670.         
  671.  
  672.  
  673.  
  674. class Attachment(LPAttachment):
  675.     """ Returns an 'Attachment'-object
  676.     
  677.     * editable attributes:
  678.         .description: any text
  679.         .contenttype: any text
  680.         .is_patch: True/False
  681.         
  682.     * read-only attributes:
  683.         .id: hash(local_filename) for local files,
  684.             launchpadlibrarian-id for files uploaded to launchpadlibrarian.net
  685.         .is_down: True if a file is downloaded to ATTACHMENT_PATH
  686.         .is_up: True if file is uploaded to launchpadlibrarian.net
  687.         ...
  688.     TODO: work on docstring
  689.     """
  690.     
  691.     def __init__(self, connection, url = None, localfilename = None, localfileobject = None, description = None, is_patch = None, contenttype = None, comment = None):
  692.         LPAttachment.__init__(self, connection, url, localfilename, localfileobject, description, is_patch, contenttype, comment)
  693.  
  694.     
  695.     def get_bugnumber(self):
  696.         if self.is_up:
  697.             return self.edit_url.split('/')[-3]
  698.  
  699.     
  700.     def get_sourcepackage(self):
  701.         if self.is_up:
  702.             return self.edit_url.split('/')[-5]
  703.  
  704.     
  705.     def get_edit_url(self):
  706.         if self.is_up:
  707.             return valid_lp_url(self._edit, BASEURL.BUG)
  708.  
  709.  
  710.  
  711. class Attachments(LPAttachments):
  712.     
  713.     def __init__(self, comments, connection, xml):
  714.         LPAttachments.__init__(self, comments = comments)
  715.         self._Attachments__xml = xml
  716.         self._Attachments__connection = connection
  717.         self._Attachments__comments = comments
  718.  
  719.     
  720.     def parse(self):
  721.         super(Attachments, self).parse()
  722.         if self._Attachments__xml:
  723.             attachments = self._Attachments__xml[0].xpathEval('li[@class="download"]')
  724.             all_att = { }
  725.             for a in attachments:
  726.                 url = a.xpathEval('a')[0].prop('href')
  727.                 edit = a.xpathEval('small/a')[0].prop('href')
  728.                 all_att[url] = edit
  729.             
  730.             for i in self:
  731.                 i._edit = all_att.get(i.url, None)
  732.                 if not (i._edit) and i.is_up:
  733.                     parse_error(False, 'Attachments.edit.1', msg = "There is an attachment (id=%s) which is added to a comment but does not appear in the sidepanel ('%s')" % (i.id, self._Attachments__comments._url), error_type = exceptions.RUNTIMEERROR)
  734.                     continue
  735.             
  736.         else:
  737.             parse_error(not (self._current), 'Attachments.edit.2', msg = "Unable to parse the 'attachments' sidepanel although there are files attached to comments ('%s')" % self._Attachments__comments._url, error_type = exceptions.RUNTIMEERROR)
  738.  
  739.     
  740.     def commit(self, force_changes = True, ignore_lp_errors = False, com_subject = None):
  741.         '''
  742.             when adding a new attachment, this attachment is added as a new comment.
  743.             this new comment has no subject but a subject is required.
  744.             setting
  745.                 force_changes=True and ignore_lp_errors=False
  746.             results in adding a subject like:
  747.                 "Re: <bug summary>"
  748.         '''
  749.         
  750.         def _lp_edit(attachment):
  751.             if not attachment.is_patch or 'on':
  752.                 pass
  753.             if not attachment.contenttype:
  754.                 pass
  755.             args = {
  756.                 'field.actions.change': '1',
  757.                 'field.title': attachment.description,
  758.                 'field.patch': 'off',
  759.                 'field.contenttype': 'text/plain' }
  760.             self._Attachments__connection.post('%s/+edit' % attachment.edit_url, args)
  761.  
  762.         
  763.         def _lp_delete(attachment):
  764.             args = {
  765.                 'field.actions.delete': '1',
  766.                 'field.title': attachment.description,
  767.                 'field.patch': 'off',
  768.                 'field.contenttype': 'text/plain' }
  769.             self._Attachments__connection.post('%s/+edit' % attachment.edit_url, args)
  770.  
  771.         
  772.         def _lp_add(attachment):
  773.             ''' delegated to comments '''
  774.             if not isinstance(attachment, Attachment):
  775.                 raise AssertionError, "<attachment> has to be an instance of 'Attachment'"
  776.             c = Comment(attachment = (attachment,))
  777.             self._Attachments__comments._lp_add_comment(c, force_changes, ignore_lp_errors, com_subject)
  778.  
  779.         changed = set(self.changed)
  780.         for i in changed:
  781.             if i.action == 'added':
  782.                 _lp_add(i.component)
  783.                 continue
  784.             (None, None, None, ((None,),))
  785.             if i.action == 'deleted':
  786.                 _lp_delete(i.component)
  787.                 continue
  788.             if i.action == 'changed':
  789.                 _lp_edit(i.component)
  790.                 continue
  791.             raise AttributeError, "Unknown action '%s' in Attachments.commit()" % i.component
  792.         
  793.  
  794.  
  795.  
  796. class Comment(LPComment):
  797.     
  798.     def __init__(self, subject = None, text = None, attachment = None):
  799.         LPComment.__init__(self, subject, text, attachment)
  800.  
  801.  
  802.  
  803. class Comments(LPComments):
  804.     
  805.     def __init__(self, connection, xml, url):
  806.         LPComments.__init__(self, url = url)
  807.         self._Comments__xml = xml
  808.         self._Comments__connection = connection
  809.         self._Comments__url = url
  810.  
  811.     
  812.     def parse(self):
  813.         for com in self._Comments__xml:
  814.             m = com.xpathEval('div[@class="boardCommentDetails"]/a[1]')
  815.             parse_error(m, 'Comments.user', xml = self._Comments__xml, url = self._Comments__url)
  816.             com_user = user.parse_html_user(m[0])
  817.             m = com.xpathEval('div[@class="boardCommentDetails"]/a[2]')
  818.             parse_error(m, 'Comments.nr', xml = self._Comments__xml, url = self._Comments__url)
  819.             com_nr = m[0].prop('href').split('/')[-1]
  820.             m = com.xpathEval('div[@class="boardCommentDetails"]/span')
  821.             parse_error(m, 'Comments.date', xml = self._Comments__xml, url = self._Comments__url)
  822.             com_date = LPTime(m[0].prop('title'))
  823.             m = com.xpathEval('div[@class="boardCommentDetails"]/a[2]/strong')
  824.             if m:
  825.                 com_subject = m[0].content
  826.             else:
  827.                 com_subject = None
  828.             m = com.xpathEval('div[@class="boardCommentBody"]/div')
  829.             parse_error(m, 'Comments.text', xml = self._Comments__xml, url = self._Comments__url)
  830.             com_text = m[0].content
  831.             com_attachments = set()
  832.             m = com.xpathEval('div[@class="boardCommentBody"]/ul/li')
  833.             for a in m:
  834.                 a_url = a.xpathEval('a').pop()
  835.                 a = re.search(',\\n +(\\S+)(;|\\))', a.content)
  836.                 parse_error(a, 'Comments.attachment.re.%s' % a_url.prop('href'), xml = com, url = self._Comments__url)
  837.                 a_contenttype = a.group(1)
  838.                 com_attachments.add(Attachment(self._Comments__connection, url = a_url.prop('href'), description = a_url.content, comment = com_nr, contenttype = a_contenttype))
  839.             
  840.             c = Comment(com_subject, com_text, com_attachments)
  841.             c.set_attr(nr = com_nr, user = com_user, date = com_date)
  842.             self.add(c)
  843.         
  844.         self._cache = self[:]
  845.         self.parsed = True
  846.         return True
  847.  
  848.     
  849.     def new(self, subject = None, text = None, attachment = None):
  850.         return Comment(subject, text, attachment, all_attachments = self._attachments)
  851.  
  852.     
  853.     def _url(self):
  854.         return self._Comments__url
  855.  
  856.     _url = property(_url)
  857.     
  858.     def _lp_add_comment(self, comment, force_changes, ignore_lp_errors, com_subject):
  859.         if not isinstance(comment, Comment):
  860.             raise AssertionError
  861.         if not comment.subject and com_subject:
  862.             pass
  863.         if not comment.text:
  864.             pass
  865.         args = {
  866.             'field.subject': 'Re:',
  867.             'field.comment': '',
  868.             'field.actions.save': '1',
  869.             'field.filecontent.used': '',
  870.             'field.email_me.used': '' }
  871.         url = self._Comments__url + '/+addcomment'
  872.         result = self._Comments__connection.post(url, args)
  873.         return result
  874.  
  875.     
  876.     def commit(self, force_changes = False, ignore_lp_errors = True, com_subject = None):
  877.         for i in self.changed:
  878.             if i.action == 'added':
  879.                 self._lp_add_comment(i.component, force_changes, ignore_lp_errors, com_subject)
  880.                 continue
  881.             raise AttributeError, "Unknown action '%s' in Comments.commit()" % i.component
  882.         
  883.  
  884.  
  885.  
  886. class Duplicates(object):
  887.     
  888.     def __init__(self, connection, xml, url):
  889.         self.parsed = False
  890.         self._Duplicates__cache = None
  891.         self._Duplicates__connection = connection
  892.         self._Duplicates__xml = xml
  893.         self._Duplicates__url = url
  894.         (self._Duplicates__duplicate_of, self._Duplicates__duplicates) = [
  895.             None] * 2
  896.  
  897.     
  898.     def __repr__(self):
  899.         return '<Duplicates>'
  900.  
  901.     
  902.     def parse(self):
  903.         if self.parsed:
  904.             return True
  905.         nodes = self._Duplicates__xml.xpathEval('//body//a[@id="duplicate-of"]')
  906.         if len(nodes) > 0:
  907.             self._Duplicates__duplicate_of = int(nodes[0].prop('href').split('/').pop())
  908.         
  909.         result = self._Duplicates__xml.xpathEval('//body//div[@class="portlet"]/h2[contains(.,"Duplicates of this bug")]/../div[@class="portletBody"]/div/ul//li/a')
  910.         self._Duplicates__duplicates = []([ int(i.prop('href').split('/')[-1]) for i in result ])
  911.         self._Duplicates__cache = self._Duplicates__duplicate_of
  912.         self.parsed = True
  913.         return True
  914.  
  915.     
  916.     def get_duplicates(self):
  917.         return self._Duplicates__duplicates
  918.  
  919.     duplicates = property(get_duplicates, doc = 'get a list of duplicates')
  920.     
  921.     def get_duplicate_of(self):
  922.         return self._Duplicates__duplicate_of
  923.  
  924.     
  925.     def set_duplicate_of(self, bugnumber):
  926.         if bugnumber == None:
  927.             self._Duplicates__duplicate_of = None
  928.         else:
  929.             self._Duplicates__duplicate_of = int(bugnumber)
  930.  
  931.     duplicate_of = property(get_duplicate_of, set_duplicate_of, doc = 'this bug report is duplicate of')
  932.     
  933.     def changed(self):
  934.         _Duplicates__changed = set()
  935.         if self._Duplicates__cache != self._Duplicates__duplicate_of:
  936.             _Duplicates__changed.add('duplicate_of')
  937.         
  938.         return frozenset(_Duplicates__changed)
  939.  
  940.     changed = property(changed)
  941.     
  942.     def commit(self, force_changes = False, ignore_lp_errors = True):
  943.         if self.changed:
  944.             if not self._Duplicates__duplicate_of:
  945.                 pass
  946.             args = {
  947.                 'field.actions.change': '1',
  948.                 'field.duplicateof': '' }
  949.             url = '%s/+duplicate' % self._Duplicates__url
  950.             result = self._Duplicates__connection.post(url, args)
  951.             if result.url == url and not ignore_lp_errors:
  952.                 x = libxml2.htmlParseDoc(result.text, 'UTF-8')
  953.                 y = x.xpathEval('//p[@class="error message"]')
  954.                 if y:
  955.                     raise ValueError, 'launchpad.net error: %s' % y[0].content
  956.                 y
  957.             
  958.         
  959.  
  960.  
  961.  
  962. class Secrecy(object):
  963.     
  964.     def __init__(self, connection, xml, url):
  965.         self.parsed = False
  966.         self._Secrecy__cache = set()
  967.         self._Secrecy__connection = connection
  968.         self._Secrecy__xml = xml
  969.         self._Secrecy__url = url
  970.         (self._Secrecy__security, self._Secrecy__private) = [
  971.             False] * 2
  972.  
  973.     
  974.     def __repr__(self):
  975.         return '<Secrecy>'
  976.  
  977.     
  978.     def parse(self):
  979.         if self.parsed:
  980.             return True
  981.         stable_xml = self._Secrecy__xml.xpathEval('//body//div[@id="big-badges"]')
  982.         if stable_xml:
  983.             if stable_xml[0].xpathEval('img[@alt="(Security vulnerability)"]'):
  984.                 self._Secrecy__security = True
  985.             
  986.             if stable_xml[0].xpathEval('img[@alt="(Private)"]'):
  987.                 self._Secrecy__private = True
  988.             
  989.         else:
  990.             self._Secrecy__private = bool(self._Secrecy__xml.xpathEval('//a[contains(@href, "+secrecy")]/strong'))
  991.             self._Secrecy__security = bool(self._Secrecy__xml.xpathEval('//div[contains(@style, "/@@/security")]'))
  992.         self._Secrecy__cache = {
  993.             'security': self._Secrecy__security,
  994.             'private': self._Secrecy__private }
  995.         self.parsed = True
  996.         return True
  997.  
  998.     
  999.     def _editlock(self):
  1000.         return bool(get_bug(id(self)).duplicate_of)
  1001.  
  1002.     
  1003.     def get_security(self):
  1004.         if not self.parsed:
  1005.             raise AssertionError, 'parse first'
  1006.         return self._Secrecy__security
  1007.  
  1008.     
  1009.     def set_security(self, security):
  1010.         self._Secrecy__security = bool(security)
  1011.  
  1012.     security = property(get_security, set_security, doc = 'security status')
  1013.     
  1014.     def get_private(self):
  1015.         if not self.parsed:
  1016.             raise AssertionError, 'parse first'
  1017.         return self._Secrecy__private
  1018.  
  1019.     
  1020.     def set_private(self, private):
  1021.         self._Secrecy__private = bool(private)
  1022.  
  1023.     private = property(get_private, set_private, doc = 'private status')
  1024.     
  1025.     def get_changed(self):
  1026.         _Secrecy__changed = set()
  1027.         for k in self._Secrecy__cache:
  1028.             if self._Secrecy__cache[k] != getattr(self, k):
  1029.                 _Secrecy__changed.add(k)
  1030.                 continue
  1031.         
  1032.         return frozenset(_Secrecy__changed)
  1033.  
  1034.     changed = property(get_changed, doc = 'get a list of changed attributes')
  1035.     
  1036.     def commit(self, force_changes = False, ignore_lp_errors = True):
  1037.         _Secrecy__url = '%s/+secrecy' % self._Secrecy__url
  1038.         status = [
  1039.             'off',
  1040.             'on']
  1041.         if self.changed:
  1042.             _Secrecy__args = {
  1043.                 'field.private': status[int(self.private)],
  1044.                 'field.security_related': status[int(self.security)],
  1045.                 'field.actions.change': 'Change' }
  1046.             _Secrecy__result = self._Secrecy__connection.post(_Secrecy__url, _Secrecy__args)
  1047.         
  1048.  
  1049.  
  1050.  
  1051. class Subscribers(LPSubscribers):
  1052.     ''' TODO:
  1053.         * change structure: use three different sets instead of one big one
  1054.     '''
  1055.     
  1056.     def __init__(self, connection, xml, url):
  1057.         self.parsed = False
  1058.         self._Subscribers__connection = connection
  1059.         self._Subscribers__xml = xml
  1060.         self._Subscribers__url = url
  1061.         LPSubscribers.__init__(self, ('directly', 'notified', 'duplicates'))
  1062.  
  1063.     
  1064.     def parse(self):
  1065.         if self.parsed:
  1066.             return True
  1067.         parse_error(self._Subscribers__xml, 'Subscribers.__xml', xml = self._Subscribers__xml, url = self._Subscribers__url)
  1068.         xml = self._Subscribers__xml[0].xpathEval('div[(@class="section" or @class="Section") and @id]')
  1069.         xml_YUI = self._Subscribers__xml[0].xpathEval('script[@type="text/javascript"]')
  1070.         if xml_YUI and not xml:
  1071.             bugnumber = int(self._Subscribers__url.split('/')[-1])
  1072.             url = 'https://launchpad.net/bugs/%i/+bug-portlet-subscribers-content' % bugnumber
  1073.             page = self._Subscribers__connection.get(url)
  1074.             ctx = libxml2.htmlParseDoc(unicode_for_libxml2(page.text), 'UTF-8')
  1075.             xml = ctx.xpathEval('//div[(@class="section" or @class="Section") and @id]')
  1076.         
  1077.         if xml:
  1078.             sections_map = {
  1079.                 'subscribers-direct': 'directly',
  1080.                 'subscribers-indirect': 'notified',
  1081.                 'subscribers-from-duplicates': 'duplicates' }
  1082.             for s in xml:
  1083.                 kind = sections_map[s.prop('id')]
  1084.                 nodes = s.xpathEval('div/a')
  1085.                 for i in nodes:
  1086.                     self[kind].add(user.parse_html_user(i))
  1087.                 
  1088.             
  1089.         elif xml_YUI:
  1090.             n = '.YUI'
  1091.         else:
  1092.             n = ''
  1093.         parse_error(False, 'Subscribers.__xml.edge.stable%s' % n, xml = self._Subscribers__xml, url = self._Subscribers__url)
  1094.         self.parsed = True
  1095.         return True
  1096.  
  1097.     
  1098.     def commit(self, force_changes = False, ignore_lp_errors = True):
  1099.         x = self.changed
  1100.  
  1101.     
  1102.     def _add(self, lplogin):
  1103.         '''Add a subscriber to a bug.'''
  1104.         url = '%s/+addsubscriber' % self._Subscribers__url
  1105.         args = {
  1106.             'field.person': lplogin,
  1107.             'field.actions.add': 'Add' }
  1108.         result = self._Subscribers__connection.post(url, args)
  1109.         if result.url == url:
  1110.             x = libxml2.htmlParseDoc(result.text, 'UTF-8')
  1111.             if x.xpathEval('//div[@class="message" and contains(.,"Invalid value")]'):
  1112.                 raise ValueError, 'Unknown Launchpad ID. You can only subscribe someone who has a Launchpad account.'
  1113.             x.xpathEval('//div[@class="message" and contains(.,"Invalid value")]')
  1114.             raise ValueError, 'Unknown error while subscribe %s to %s' % (lplogin, url)
  1115.         result.url == url
  1116.         return result
  1117.  
  1118.     
  1119.     def _remove(self, lplogin):
  1120.         '''Remove a subscriber from a bug.'''
  1121.         url = '%s/' % self._Subscribers__url
  1122.         args = {
  1123.             'field.subscription': lplogin,
  1124.             'unsubscribe': 'Continue' }
  1125.         result = self._Subscribers__connection.post(url, args)
  1126.         return result
  1127.  
  1128.  
  1129.  
  1130. class ActivityWhat(str):
  1131.     
  1132.     def __new__(cls, what, url = None):
  1133.         obj = super(ActivityWhat, cls).__new__(ActivityWhat, what)
  1134.         obj._ActivityWhat__task = None
  1135.         obj._ActivityWhat__attribute = None
  1136.         x = what.split(':')
  1137.         if len(x) == 2:
  1138.             obj._ActivityWhat__task = x[0]
  1139.             obj._ActivityWhat__attribute = x[1].strip()
  1140.         elif len(x) == 1:
  1141.             obj._ActivityWhat__attribute = x[0]
  1142.         else:
  1143.             raise ValueError
  1144.         return len(x) == 2
  1145.  
  1146.     
  1147.     def task(self):
  1148.         return self._ActivityWhat__task
  1149.  
  1150.     task = property(task)
  1151.     
  1152.     def attribute(self):
  1153.         return self._ActivityWhat__attribute
  1154.  
  1155.     attribute = property(attribute)
  1156.  
  1157.  
  1158. class Activity(object):
  1159.     
  1160.     def __init__(self, date, user, what, old_value, new_value, message):
  1161.         self._Activity__date = date
  1162.         self._Activity__user = user
  1163.         self._Activity__what = what
  1164.         self._Activity__old_value = old_value
  1165.         self._Activity__new_value = new_value
  1166.         self._Activity__message = message
  1167.  
  1168.     
  1169.     def __repr__(self):
  1170.         return "<%s %s '%s'>" % (self.user, self.date, self.what)
  1171.  
  1172.     
  1173.     def date(self):
  1174.         return self._Activity__date
  1175.  
  1176.     date = property(date)
  1177.     
  1178.     def user(self):
  1179.         return self._Activity__user
  1180.  
  1181.     user = property(user)
  1182.     
  1183.     def what(self):
  1184.         return self._Activity__what
  1185.  
  1186.     what = property(what)
  1187.     
  1188.     def old_value(self):
  1189.         return self._Activity__old_value
  1190.  
  1191.     old_value = property(old_value)
  1192.     
  1193.     def new_value(self):
  1194.         return self._Activity__new_value
  1195.  
  1196.     new_value = property(new_value)
  1197.     
  1198.     def message(self):
  1199.         return self._Activity__message
  1200.  
  1201.     message = property(message)
  1202.  
  1203.  
  1204. class ActivityLog(object):
  1205.     '''
  1206.     TODO: there is nor clear relation between an entry in the activity log
  1207.         and a certain task, this is why the result of when(), completed(),
  1208.         assigned() and started_work() may differ from the grey infobox added
  1209.         to each task. Maybe we should also parse this box.
  1210.     '''
  1211.     
  1212.     def __init__(self, connection, url):
  1213.         self.parsed = False
  1214.         self._ActivityLog__connection = connection
  1215.         self._ActivityLog__activity = []
  1216.         self._ActivityLog__url = url
  1217.  
  1218.     
  1219.     def __repr__(self):
  1220.         return '<activity log>'
  1221.  
  1222.     
  1223.     def __str__(self):
  1224.         return str(self._ActivityLog__activity)
  1225.  
  1226.     
  1227.     def __iter__(self):
  1228.         for i in self.activity:
  1229.             yield i
  1230.         
  1231.  
  1232.     
  1233.     def __getitem__(self, key):
  1234.         return self.activity[key]
  1235.  
  1236.     
  1237.     def __len__(self):
  1238.         return len(self.activity)
  1239.  
  1240.     
  1241.     def activity(self):
  1242.         if not self.parsed:
  1243.             self.parse()
  1244.         
  1245.         return self._ActivityLog__activity
  1246.  
  1247.     activity = property(activity)
  1248.     
  1249.     def _activity_rev(self):
  1250.         if not self.parsed:
  1251.             self.parse()
  1252.         
  1253.         return self._ActivityLog__activity_rev
  1254.  
  1255.     
  1256.     def parse(self):
  1257.         if self.parsed:
  1258.             return True
  1259.         page = self._ActivityLog__connection.get('%s/+activity' % self._ActivityLog__url)
  1260.         self._ActivityLog__xmldoc = libxml2.htmlParseDoc(unicode_for_libxml2(page.text), 'UTF-8')
  1261.         table = self._ActivityLog__xmldoc.xpathEval('//body//table[@class="listing"][1]//tbody//tr')
  1262.         parse_error(table, 'ActivityLog.__table', xml = self._ActivityLog__xmldoc, url = self._ActivityLog__url)
  1263.         for row in table:
  1264.             r = row.xpathEval('td')
  1265.             parse_error(len(r) == 6, 'ActivityLog.len(td)', xml = row, url = self._ActivityLog__url)
  1266.             date = LPTime(r[0].content)
  1267.             x = r[1].xpathEval('a')
  1268.             parse_error(x, 'ActivityLog.lp_user', xml = row, url = self._ActivityLog__url)
  1269.             lp_user = user.parse_html_user(x[0])
  1270.             
  1271.             try:
  1272.                 what = ActivityWhat(r[2].content)
  1273.             except ValueError:
  1274.                 self.parsed
  1275.                 self.parsed
  1276.                 parse_error(False, 'ActivityLog.ActivityWhat', xml = self._ActivityLog__xmldoc, url = '%s/+activity' % self._ActivityLog__url)
  1277.             except:
  1278.                 self.parsed
  1279.  
  1280.             old_value = r[3].content
  1281.             new_value = r[4].content
  1282.             message = r[5].content
  1283.             self._ActivityLog__activity.append(Activity(date, lp_user, what, old_value, new_value, message))
  1284.         
  1285.         self._ActivityLog__activity_rev = self._ActivityLog__activity[::-1]
  1286.         self.parsed = True
  1287.         return True
  1288.  
  1289.     
  1290.     def assigned(self, task):
  1291.         for i in self._activity_rev():
  1292.             if i.what.task == task and i.what.attribute == 'assignee':
  1293.                 return i.date
  1294.         
  1295.  
  1296.     
  1297.     def completed(self, task):
  1298.         for i in self._activity_rev():
  1299.             if i.what.task == task and i.what.attribute == 'status' and i.new_value in ('Invalid', 'Fix Released'):
  1300.                 return i.date
  1301.         
  1302.  
  1303.     
  1304.     def when(self, task):
  1305.         for i in self._activity_rev():
  1306.             if i.what == 'bug' and i.message.startswith('assigned to') and i.message.count(task):
  1307.                 return i.date
  1308.         
  1309.  
  1310.     
  1311.     def started_work(self, task):
  1312.         for i in self._activity_rev():
  1313.             if i.what.task == task and i.what.attribute == 'status' and i.new_value in ('In Progress', 'Fix Committed'):
  1314.                 return i.date
  1315.         
  1316.  
  1317.  
  1318.  
  1319. class Mentoring(object):
  1320.     
  1321.     def __init__(self, connection, xml, url):
  1322.         self.parsed = False
  1323.         self._Mentoring__cache = set()
  1324.         self._Mentoring__connection = connection
  1325.         self._Mentoring__xml = xml
  1326.         self._Mentoring__url = url
  1327.         self._Mentoring__mentor = set()
  1328.  
  1329.     
  1330.     def __repr__(self):
  1331.         return '<Mentor for #%s>' % self._Mentoring__url
  1332.  
  1333.     
  1334.     def parse(self):
  1335.         if self.parsed:
  1336.             return True
  1337.         for i in self._Mentoring__xml:
  1338.             self._Mentoring__mentor.add(user.parse_html_user(i))
  1339.         
  1340.         self._Mentoring__cache = self._Mentoring__mentor.copy()
  1341.         self.parsed = True
  1342.         return True
  1343.  
  1344.     
  1345.     def mentor(self):
  1346.         return self._Mentoring__mentor
  1347.  
  1348.     mentor = property(mentor)
  1349.     
  1350.     def changed(self):
  1351.         '''get a list of changed attributes
  1352.         currently read-only
  1353.         '''
  1354.         return set()
  1355.  
  1356.     changed = property(changed)
  1357.     
  1358.     def commit(self, force_changes = False, ignore_lp_errors = True):
  1359.         raise NotImplementedError, 'this method is not implemented ATM'
  1360.  
  1361.  
  1362.  
  1363. class BzrBranch(object):
  1364.     
  1365.     def __init__(self, title, url, status):
  1366.         self._BzrBranch__title = title
  1367.         self._BzrBranch__url = url
  1368.         self._BzrBranch__status = status
  1369.  
  1370.     
  1371.     def __repr__(self):
  1372.         return 'BzrBranch(%s, %s, %s)' % (self.title, self.url, self.status)
  1373.  
  1374.     __str__ = __repr__
  1375.     
  1376.     def title(self):
  1377.         return self._BzrBranch__title
  1378.  
  1379.     title = property(title)
  1380.     
  1381.     def url(self):
  1382.         return self._BzrBranch__url
  1383.  
  1384.     url = property(url)
  1385.     
  1386.     def status(self):
  1387.         return self._BzrBranch__status
  1388.  
  1389.     status = property(status)
  1390.  
  1391.  
  1392. class Branches(set):
  1393.     
  1394.     def __init__(self, connection, xml, url):
  1395.         self.parsed = False
  1396.         set.__init__(self)
  1397.         self._Branches__url = url
  1398.         self._Branches__xml = xml
  1399.         self._Branches__connection = connection
  1400.  
  1401.     
  1402.     def parse(self):
  1403.         if self.parsed:
  1404.             return True
  1405.         for i in self._Branches__xml:
  1406.             m = i.xpathEval('a[1]')
  1407.             if not m:
  1408.                 raise AssertionError
  1409.             title = m[0].prop('title')
  1410.             url = m[0].prop('href')
  1411.             m = i.xpathEval('span')
  1412.             if not m:
  1413.                 raise AssertionError
  1414.             status = m[0].content
  1415.             self.add(BzrBranch(title, url, status))
  1416.         
  1417.         self.parsed = True
  1418.  
  1419.     
  1420.     def changed(self):
  1421.         '''get a list of changed attributes
  1422.         currently read-only
  1423.         '''
  1424.         return set()
  1425.  
  1426.     changed = property(changed)
  1427.     
  1428.     def commit(self, force_changes = False, ignore_lp_errors = True):
  1429.         raise NotImplementedError, 'this method is not implemented ATM'
  1430.  
  1431.  
  1432.  
  1433. class Bug(BugBase):
  1434.     _container_refs = { }
  1435.     
  1436.     def __init__(self, bug = None, url = None, connection = None):
  1437.         BugBase.__init__(self, bug, url, connection)
  1438.         bugpage = self._Bug__connection.get(self._Bug__url)
  1439.         self._Bug__text = bugpage.text
  1440.         self._Bug__url = bugpage.url
  1441.         self.xmldoc = libxml2.htmlParseDoc(unicode_for_libxml2(self._Bug__text), 'UTF-8')
  1442.         self._Bug__bugreport = BugReport(connection = self._Bug__connection, xml = self.xmldoc, url = self._Bug__url)
  1443.         self._Bug__infotable = InfoTable(connection = self._Bug__connection, xml = self.xmldoc.xpathEval('//body//table[@class="listing" or @class="duplicate listing"][1]'), url = self._Bug__url)
  1444.         self._Bug__comments = Comments(connection = self._Bug__connection, xml = self.xmldoc.xpathEval('//body//div[normalize-space(@class)="boardComment"]'), url = self._Bug__url)
  1445.         self._Bug__attachments = Attachments(self._Bug__comments, self._Bug__connection, self.xmldoc.xpathEval('//body//div[@id="portlet-attachments"]/div/div/ul'))
  1446.         self._Bug__duplicates = Duplicates(connection = self._Bug__connection, xml = self.xmldoc, url = self._Bug__url)
  1447.         self._Bug__secrecy = Secrecy(connection = self._Bug__connection, xml = self.xmldoc, url = self._Bug__url)
  1448.         self._Bug__subscribers = Subscribers(connection = self._Bug__connection, xml = self.xmldoc.xpathEval('//body//div[@id="portlet-subscribers"]'), url = self._Bug__url)
  1449.         self._Bug__mentor = Mentoring(connection = self._Bug__connection, xml = self.xmldoc.xpathEval('//body//img [@src="/@@/mentoring"]/parent::p//a'), url = self._Bug__url)
  1450.         self._Bug__activity = ActivityLog(connection = self._Bug__connection, url = self._Bug__url)
  1451.         self._Bug__branches = Branches(self._Bug__connection, self.xmldoc.xpathEval('//body//div[@class="bug-branch-summary"]'), self._Bug__url)
  1452.         Bug._container_refs[id(self._Bug__attachments)] = self
  1453.         Bug._container_refs[id(self._Bug__comments)] = self
  1454.         Bug._container_refs[id(self._Bug__secrecy)] = self
  1455.  
  1456.     
  1457.     def __del__(self):
  1458.         '''run self.xmldoc.freeDoc() to clear memory'''
  1459.         if hasattr(self, 'xmldoc'):
  1460.             self.xmldoc.freeDoc()
  1461.         
  1462.  
  1463.     
  1464.     def changed(self):
  1465.         _Bug__result = []
  1466.         for i in filter((lambda a: a.startswith('_Bug__')), dir(self)):
  1467.             
  1468.             try:
  1469.                 a = getattr(self.__dict__[i], 'changed')
  1470.             except AttributeError:
  1471.                 continue
  1472.  
  1473.             if a:
  1474.                 _Bug__result.append(change_obj(self.__dict__[i]))
  1475.                 continue
  1476.         
  1477.         return _Bug__result
  1478.  
  1479.     changed = property(changed)
  1480.     
  1481.     def commit(self, force_changes = False, ignore_lp_errors = True):
  1482.         for i in self.changed:
  1483.             if isinstance(i.component, Comments):
  1484.                 result = i.component.commit(force_changes, ignore_lp_errors, 'Re: %s' % self.title)
  1485.                 continue
  1486.             result = i.component.commit(force_changes, ignore_lp_errors)
  1487.         
  1488.  
  1489.     
  1490.     def revert(self):
  1491.         ''' need a function to revert changes '''
  1492.         pass
  1493.  
  1494.     get_url = _gen_getter('__url')
  1495.     get_bugnumber = _gen_getter('__bugnumber')
  1496.     get_reporter = _gen_getter('__bugreport.reporter')
  1497.     get_date = _gen_getter('__bugreport.date')
  1498.     get_duplicates = _gen_getter('__duplicates.duplicates')
  1499.     get_description_raw = _gen_getter('__bugreport.description_raw')
  1500.     get_activity = _gen_getter('__activity')
  1501.     
  1502.     def get_text(self):
  1503.         return '\n'.join % ([], []([ c.text for c in self.comments ]))
  1504.  
  1505.     get_title = _gen_getter('__bugreport.title')
  1506.     get_description = _gen_getter('__bugreport.description')
  1507.     set_description = _gen_setter('__bugreport.description')
  1508.     get_tags = _gen_getter('__bugreport.tags')
  1509.     get_nickname = _gen_getter('__bugreport.nickname')
  1510.     get_mentors = _gen_getter('__mentor.mentor')
  1511.     get_infotable = _gen_getter('__infotable')
  1512.     get_info = _blocked(_gen_getter('__infotable.current'), "No 'current' available.")
  1513.     get_target = _blocked(_gen_getter('__infotable.current.target'), "Can't get 'target'.")
  1514.     get_importance = _blocked(_gen_getter('__infotable.current.importance'), "Can't get 'importance'.")
  1515.     set_importance = _blocked(_gen_setter('__infotable.current.importance'), "Can't set 'importance'.")
  1516.     get_status = _blocked(_gen_getter('__infotable.current.status'), "Can't get 'status'.")
  1517.     set_status = _blocked(_gen_setter('__infotable.current.status'), "Can't set 'status'.")
  1518.     get_assignee = _blocked(_gen_getter('__infotable.current.assignee'), "Can't get 'assignee'.")
  1519.     set_assignee = _blocked(_gen_setter('__infotable.current.assignee'), "Can't set 'assignee'.")
  1520.     get_milestone = _blocked(_gen_getter('__infotable.current.milestone'), "Can't get 'milestone'.")
  1521.     set_milestone = _blocked(_gen_setter('__infotable.current.milestone'), "Can't set 'milestone'.")
  1522.     get_sourcepackage = _blocked(_gen_getter('__infotable.current.sourcepackage'), "Can't get 'sourcepackage'.")
  1523.     set_sourcepackage = _blocked(_gen_setter('__infotable.current.sourcepackage'), "Can't set 'sourcepackage'.")
  1524.     get_affects = _blocked(_gen_getter('__infotable.current.affects'), "Can't get 'affects'.")
  1525.     
  1526.     def has_target(self, target):
  1527.         if not self._Bug__infotable.parsed:
  1528.             self._Bug__infotable.parse()
  1529.         
  1530.         return self._Bug__infotable.has_target(target)
  1531.  
  1532.     get_duplicates = _gen_getter('__duplicates.duplicates')
  1533.     get_duplicate = _gen_getter('__duplicates.duplicate_of')
  1534.     set_duplicate = _gen_setter('__duplicates.duplicate_of')
  1535.     get_security = _gen_getter('__secrecy.security')
  1536.     set_security = _gen_setter('__secrecy.security')
  1537.     get_private = _gen_getter('__secrecy.private')
  1538.     set_private = _gen_setter('__secrecy.private')
  1539.     get_subscriptions = _gen_getter('__subscribers')
  1540.     get_attachments = _gen_getter('__attachments')
  1541.     
  1542.     def get_comments(self):
  1543.         x = self.attachments
  1544.         if not self._Bug__comments.parsed:
  1545.             self.comments.parse()
  1546.         
  1547.         return self._Bug__comments
  1548.  
  1549.     
  1550.     def get_subscriptions_category(self, type):
  1551.         ''' get subscriptions for a given category, possible categories are "directly", "notified", "duplicates" '''
  1552.         if not self._Bug__subscribers.parsed:
  1553.             self._Bug__subscribers.parse()
  1554.         
  1555.         return self._Bug__subscribers.get_subscriptions(type)
  1556.  
  1557.     get_branches = _gen_getter('__branches')
  1558.  
  1559.  
  1560. def create_new_bugreport(product, summary, description, connection, tags = [], security_related = False):
  1561.     ''' creates a new bugreport and returns its bug object
  1562.     
  1563.         product keys: "name", "target" (optional)
  1564.         tags: list of tags
  1565.     '''
  1566.     args = {
  1567.         'field.title': summary,
  1568.         'field.comment': description,
  1569.         'field.actions.submit_bug': 1 }
  1570.     if tags:
  1571.         args['field.tags'] = ' '.join(tags)
  1572.     
  1573.     if security_related:
  1574.         args['field.security_related'] = 'on'
  1575.     
  1576.     if product.has_key('target'):
  1577.         url = 'https://bugs.launchpad.net/%s/+source/%s/+filebug-advanced' % (product['target'], product['name'])
  1578.         args['field.packagename'] = product['name']
  1579.     else:
  1580.         url = 'https://bugs.launchpad.net/%s/+filebug-advanced' % product['name']
  1581.     
  1582.     try:
  1583.         result = connection.post(url, args)
  1584.     except exceptions.LaunchpadError:
  1585.         e = None
  1586.         
  1587.         try:
  1588.             x = connection.needs_login()
  1589.         except exceptions.LaunchpadError:
  1590.             x = False
  1591.  
  1592.         if x:
  1593.             raise exceptions.LaunchpadLoginError(url)
  1594.         x
  1595.         if isinstance(e, exceptions.LaunchpadURLError):
  1596.             if 'Page not found' in e.msg:
  1597.                 m = "Maybe there is no product '%s' in " % product['name']
  1598.                 if product.has_key('target'):
  1599.                     m += "the distribution '%s'" % product['target']
  1600.                 else:
  1601.                     m += 'launchpad.net'
  1602.                 raise exceptions.PythonLaunchpadBugsValueError(msg = m)
  1603.             'Page not found' in e.msg
  1604.         else:
  1605.             raise 
  1606.         isinstance(e, exceptions.LaunchpadURLError)
  1607.  
  1608.     if not result.url.endswith('+filebug-advanced'):
  1609.         return Bug(url = result.url, connection = connection)
  1610.     raise exceptions.choose_pylpbugsError(error_type = exceptions.VALUEERROR, text = result.text, url = url)
  1611.  
  1612.